﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace System.Threading.Actors.Utilities
{
    // Code (mostly) shamelessly lifted from http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266920.aspx
    public class AsyncAutoResetEvent : IDisposable
    {
        // A task to return when someone calls WaitAsync when we are already signaled
        private readonly static Task s_completed = Task.FromResult(true);

        // A queue for waiters
        private readonly Queue<TaskCompletionSource<bool>> m_waits = new Queue<TaskCompletionSource<bool>>();
        
        // The current state of our signal
        private volatile bool m_signaled;

        // Tracks object disposal
        private volatile bool m_disposed;


        // Asynchronously wait for the auto-reset event to become signaled.
        public Task WaitAsync()
        {
            lock (m_waits)
            {
                if (m_disposed) throw new ObjectDisposedException(null, "Attempted to call WaitAsync on a disposed AsyncAutoResetEvent");

                if (m_signaled)
                {
                    // We're signaled.  So set ourselves to unsignaled and return a completed task.
                    m_signaled = false;
                    return s_completed;
                }
                else
                {
                    // We're not signaled.  Create a tcs, stick it on the wait queue, and return tcs.Task.
                    var tcs = new TaskCompletionSource<bool>();
                    m_waits.Enqueue(tcs);
                    return tcs.Task;
                }
            }
        }

        /// <summary>
        /// Signal the auto-reset event 
        /// </summary>
        /// <param name="underLock">An indication of whether or not the setter is under lock.  If so, then the waking up of waiters needs to
        /// be scheduled on a separate thread, so that an await does not wake up under lock.</param>
        public void Set(bool underLock = false)
        {
            TaskCompletionSource<bool> toRelease = null;
            lock (m_waits)
            {
                if (m_disposed) 
                    throw new ObjectDisposedException(null, "Attempted to call Set on a disposed AsyncAutoResetEvent");

                if (m_waits.Count > 0)
                {
                    // We have a waiter.  Release it and stay in unsignaled state.
                    toRelease = m_waits.Dequeue();
                }
                else if (!m_signaled)
                {
                    // No one is waiting.  Set ourselves to signaled so that the next WaitAsync call can proceed.
                    m_signaled = true;
                }
            }

            // If you release a waiter, do so outside of the lock.  If the waiter has synchronous continuations,
            // they will run as part of this call, and we don't want that to happen inside the lock.
            if (toRelease != null)
            {
                if (underLock)
                {
                    // If we are under lock on the producer, we should schedule the SetResult on another thread.
                    Task.Factory.StartNew(tcsAsObject =>
                    {
                        ((TaskCompletionSource<bool>)tcsAsObject).SetResult(true);
                    }, toRelease, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
                }
                else
                {
                    toRelease.SetResult(true);
                }
            }
        }

        // Dispose.  Faults remaining waiters.
        public void Dispose()
        {
            if (!m_disposed)
            {
                List<TaskCompletionSource<bool>> toFaultList = null; 
                lock (m_waits)
                {
                    if (!m_disposed)
                    {
                        m_disposed = true;
                        toFaultList = new List<TaskCompletionSource<bool>>(m_waits); // record remaining waiters, to be faulted
                        m_waits.Clear();
                    }

                }

                // Fault remaining waiters outside of the lock.
                if (toFaultList != null)
                {
                    var ode = new ObjectDisposedException(null, "The AsyncAutoResetEvent upon which you were waiting was disposed");
                    foreach (var tcs in toFaultList) tcs.TrySetException(ode);
                }
            }
        }
    }
}
